/*
 * Copyright (c) 2025 Element Creations Ltd.
 * Copyright 2024, 2025 New Vector Ltd.
 *
 * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
 * Please see LICENSE files in the repository root for full details.
 */

package extension

import kotlinx.kover.gradle.plugin.dsl.AggregationType
import kotlinx.kover.gradle.plugin.dsl.CoverageUnit
import kotlinx.kover.gradle.plugin.dsl.GroupingEntityType
import kotlinx.kover.gradle.plugin.dsl.KoverProjectExtension
import kotlinx.kover.gradle.plugin.dsl.KoverVariantCreateConfig
import org.gradle.api.Action
import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.assign

enum class KoverVariant(val variantName: String) {
    Presenters("presenters"),
    States("states"),
    Views("views"),
}

val koverVariants = KoverVariant.values().map { it.variantName }

val localAarProjects = listOf(
    ":libraries:rustsdk",
    ":libraries:textcomposer:lib"
)

val excludedKoverSubProjects = listOf(
    ":app",
    ":annotations",
    ":codegen",
    ":tests:testutils",
    // Exclude modules which are not Android libraries
    // See https://github.com/Kotlin/kotlinx-kover/issues/312
    ":appconfig",
    ":libraries:core",
    ":libraries:coroutines",
    ":libraries:di",
) + localAarProjects

private fun Project.kover(action: Action<KoverProjectExtension>) {
    (this as org.gradle.api.plugins.ExtensionAware).extensions.configure("kover", action)
}

fun Project.setupKover() {
    // Create verify all task joining all existing verification tasks
    tasks.register("koverVerifyAll") {
        group = "verification"
        description = "Verifies the code coverage of all subprojects."
        val dependencies = listOf(":app:koverVerifyGplayDebug") + koverVariants.map { ":app:koverVerify${it.replaceFirstChar(Char::titlecase)}" }
        dependsOn(dependencies)
    }
    // https://kotlin.github.io/kotlinx-kover/
    // Run `./gradlew :app:koverHtmlReport` to get report at ./app/build/reports/kover
    // Run `./gradlew :app:koverXmlReport` to get XML report
    kover {
        reports {
            filters {
                excludes {
                    classes(
                        // Exclude generated classes.
                        "*_Module",
                        "*_AssistedFactory",
                        "com.airbnb.android.showkase*",
                        "io.element.android.libraries.designsystem.showkase.*",
                        "*ComposableSingletons$*",
                        "*BuildConfig",
                        // Generated by Showkase
                        "*Ioelementandroid*PreviewKt$*",
                        "*Ioelementandroid*PreviewKt",
                        // Other
                        // We do not cover Nodes (normally covered by maestro, but code coverage is not computed with maestro)
                        "*Node",
                        "*Node$*",
                        "*Presenter\$present\$*",
                        // Forked from compose
                        "io.element.android.libraries.designsystem.theme.components.bottomsheet.*",
                        // Konsist code to make test fails
                        "io.element.android.tests.konsist.failures",
                    )
                    annotatedBy(
                        "androidx.compose.ui.tooling.preview.Preview",
                        "io.element.android.libraries.architecture.coverage.ExcludeFromCoverage",
                        "io.element.android.libraries.designsystem.preview.*",
                    )
                }
            }

            total {
                verify {
                    // General rule: minimum code coverage.
                    rule("Global minimum code coverage.") {
                        groupBy = GroupingEntityType.APPLICATION
                        bound {
                            minValue = 70
                            // Setting a max value, so that if coverage is bigger, it means that we have to change minValue.
                            // For instance if we have minValue = 20 and maxValue = 30, and current code coverage is now 31.32%, update
                            // minValue to 25 and maxValue to 35.
                            maxValue = 80
                            coverageUnits = CoverageUnit.INSTRUCTION
                            aggregationForGroup = AggregationType.COVERED_PERCENTAGE
                        }
                    }
                }
            }
            variant(KoverVariant.Presenters.variantName) {
                verify {
                    // Rule to ensure that coverage of Presenters is sufficient.
                    rule("Check code coverage of presenters") {
                        groupBy = GroupingEntityType.CLASS

                        bound {
                            minValue = 85
                            coverageUnits = CoverageUnit.INSTRUCTION
                            aggregationForGroup = AggregationType.COVERED_PERCENTAGE
                        }
                    }
                }
                filters {
                    excludes.classes(
                        "*Fake*Presenter*",
                        "io.element.android.appnav.loggedin.LoggedInPresenter$*",
                        // Some options can't be tested at the moment
                        "io.element.android.features.preferences.impl.developer.DeveloperSettingsPresenter$*",
                        // Need an Activity to use rememberMultiplePermissionsState
                        "io.element.android.features.location.impl.common.permissions.DefaultPermissionsPresenter",
                        "*Presenter\$present\$*",
                        // Too small to be > 85% tested
                        "io.element.android.libraries.fullscreenintent.impl.DefaultFullScreenIntentPermissionsPresenter",
                    )
                    includes.inheritedFrom("io.element.android.libraries.architecture.Presenter")
                }
            }
            variant(KoverVariant.States.variantName) {
                verify {
                    // Rule to ensure that coverage of States is sufficient.
                    rule("Check code coverage of states") {
                        groupBy = GroupingEntityType.CLASS
                        bound {
                            minValue = 90
                            coverageUnits = CoverageUnit.INSTRUCTION
                            aggregationForGroup = AggregationType.COVERED_PERCENTAGE
                        }
                    }
                }
                filters {
                    excludes.classes(
                        "*State$*", // Exclude inner classes
                        "io.element.android.appnav.root.RootNavState",
                        "io.element.android.features.ftue.api.state.*",
                        "io.element.android.features.ftue.impl.welcome.state.*",
                        "io.element.android.features.messages.impl.timeline.model.bubble.BubbleState",
                        "io.element.android.libraries.designsystem.swipe.SwipeableActionsState",
                        "io.element.android.libraries.designsystem.theme.components.bottomsheet.CustomSheetState",
                        "io.element.android.libraries.maplibre.compose.CameraPositionState",
                        "io.element.android.libraries.maplibre.compose.SymbolState",
                        "io.element.android.libraries.matrix.api.room.RoomMembershipState",
                        "io.element.android.libraries.matrix.api.room.RoomMembersState",
                        "io.element.android.libraries.matrix.api.timeline.item.event.OtherState$*",
                        "io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState*",
                        "io.element.android.libraries.mediaviewer.impl.local.pdf.PdfViewerState",
                        "io.element.android.libraries.mediaviewer.impl.local.player.MediaPlayerControllerState",
                        "io.element.android.libraries.textcomposer.model.TextEditorState",
                        "io.element.android.libraries.textcomposer.components.FormattingOptionState",
                    )
                    includes.classes("*State")
                }
            }
            variant(KoverVariant.Views.variantName) {
                verify {
                    // Rule to ensure that coverage of Views is sufficient (deactivated for now).
                    rule("Check code coverage of views") {
                        groupBy = GroupingEntityType.CLASS
                        bound {
                            // TODO Update this value, for now there are too many missing tests.
                            minValue = 0
                            coverageUnits = CoverageUnit.INSTRUCTION
                            aggregationForGroup = AggregationType.COVERED_PERCENTAGE
                        }
                    }
                }
                filters {
                    excludes.classes("*ViewKt$*") // Exclude inner classes
                    includes.classes("*ViewKt")
                }
            }
        }
    }
}

fun Project.applyKoverPluginToAllSubProjects() = rootProject.subprojects {
    if (project.path !in localAarProjects) {
        apply(plugin = "org.jetbrains.kotlinx.kover")
        kover {
            currentProject {
                for (variant in koverVariants) {
                    createVariant(variant) {
                        defaultVariants(project)
                    }
                }
            }
        }

        project.afterEvaluate {
            for (variant in koverVariants) {
                // Using the cache for coverage verification seems to be flaky, so we disable it for now.
                val taskName = "koverCachedVerify${variant.replaceFirstChar(Char::titlecase)}"
                val cachedTask = project.tasks.findByName(taskName)
                cachedTask?.let {
                    it.outputs.upToDateWhen { false }
                }
            }
        }
    }
}

fun KoverVariantCreateConfig.defaultVariants(project: Project) {
    if (project.name == "app") {
        addWithDependencies("gplayDebug")
    } else {
        addWithDependencies("debug", "jvm", optional = true)
    }
}

fun Project.koverSubprojects() = project.rootProject.subprojects
    .filter {
        it.project.projectDir.resolve("build.gradle.kts").exists()
    }
    .map { it.path }
    .sorted()
    .filter {
        it !in excludedKoverSubProjects
    }

fun Project.koverDependencies() {
    project.koverSubprojects()
        .forEach {
            // println("Add $it to kover")
            dependencies.add("kover", project(it))
        }
}
